#!/usr/opt/bin/python

""":Cue:Cat scanner tool. cuecat.py [+type ...] [command to run]

When invoked without arguments, it prompts the user to scan, and prints the
decoded and disassembled cuecat scan.

If there are no arguments at all, then the values from the :Cue:Cat -
the scanner Id, the barcode's Type and the Code value - are printed.

If invoked with any arguments, then an attempt is made to convert the
bar codes to EAN13 codes. Failing to convert the code is generates an
error.

Arguments of the form "+type" indicate that only arguments the
:Cue:Cat describes as "type" - for all the types listed - should be
processed. All others are ignored before any conversion attempt is
made.

Two types of barcodes get special handling if they are filtered
for. Any type starting with "IB" is converted into an ISBN, and the
ISBN is used instead of the EAN13 code. Any type starting with "UP" is
converted into a UPC code, and that is used instead of the EAN13
version of the barcode.

The "command to run" should contain a single %s, which will be
replaced with the scanned code. The command is then executed. After it
returns, the next value is prompted for.

See the man page for example uses.  And yes, this interface is a
kludge."""

import sys, os, string, binascii, operator
from os import system

import barcode

class cuecat:
    """Internal representation of a :Cue:Cat wand scanner.

    All methods will raise ValueError if presented with an invalid bar code.
    They are:
    	Scan - read in a new scan code. Optional arguments are "ascii",
        	a string to be scanned (instead of reading it via raw_input),
                and "prompt", a string to pass to raw_input (ignored if
                "ascii" is set).
    	Loop - A utility to loop reading scan codes and invoking a the
        	caller's "callback" object. If "callback" has a true
                "read" attribute, that is invoked as a method to get a
                string to be scanned. If "callback" has a "prompt" attribute,
                that is passed to Scan as the "prompt" argument. Returns on
                EOF, or if the invocation of callback returns true. If
                Scan returns a ValueError and "callback" has a true "error"
                attribute, then that is invoked with the value from the
                exception. Otherwise, the exception is reraised.
    	Decode - returns a tuple of the CueCat's Id, most recently scanned
        	values Type, and the barcode from the most recent scan.
    	Id, Type, Code - methods to return the Id, Type and Code from the most
        	recent scan.

    The one attribute is "Raw", the raw string from the scan."""

    transtable = string.maketrans(string.join(map(chr,
                                                  range(ord('a'), ord('z')+1) +
                                                  range(ord('A'), ord('Z')+1) +
                                                  range(ord('0'), ord('9')+1) +
                                                  [ord('+'), ord('-')]),
                                              ''),
                                  string.join(map(chr,
                                                  range(ord(' '), ord('_')+1)),
                                              ''))
    key = ord("C")

    def Scan(my, ascii = None, prompt = None):
        "Decode and parse - possibly after scanning - a new scanner value."

        if not ascii:
            ascii = raw_input(prompt or 'Scan! ')
        raw = string.split(ascii, '.')
        if len(raw) != 5: raise ValueError, "Invalid bar code: " + ascii
        my.Raw = raw[1:4]	# Toss header & newline
        my.id = my.type = my.code = ''

    def Loop(my, callback):
        """Loop scanning input lines and invoking callback object on myself.

        If callback has a read attribute, that is invoked to obtain a string
        to pass to scan.

        If callback has a prompt attribute, that is passed to Scan as
        a prompt.

        EOFError or callback returning true causes a return.
        Other errors are passed through to the caller."""

        while 1:
            try:
                if hasattr(callback, 'read') and callback.read:
                    my.Scan(callback.read())
                elif hasattr(callback, 'prompt'):
                    my.Scan(prompt = callback.prompt)
                else: my.Scan()
                my.Decode()
            except ValueError, message:
                if hasattr(callback, "error"): callback.error(my, message)
                else: raise
            except EOFError:
                return
            else:
                if callback(my): return

    def Id(my):
        "Returns decoded id."

        if not my.id: my.id = my.decodestring(my.Raw[0])
        return my.id

    def Type(my):
        "Returns decoded type."
        
        if not my.type: my.type = my.decodestring(my.Raw[1])
        return my.type

    def Code(my):
        "Returns decoded bar code."
        
        if not my.code: my.code = my.decodestring(my.Raw[2])
        return my.code

    def Decode(my):
        "Returns decoded tuple."

        return my.Id(), my.Type(), my.Code()

    def decodestring(my, value):
        "Internal string decoder"

        translated = string.translate(value, my.transtable)
        unpacked = binascii.a2b_uu(chr(32 + 3 * len(translated) / 4) +
                                   translated)
        try: truncated = unpacked[: string.index(unpacked, '\0')]
        except ValueError: truncated = unpacked
        bytelist =  map(operator.xor, map(ord, truncated),
                        len(truncated) * [my.key])
        return string.join(map(chr, bytelist), '')


class show:
    "A simple cuecat code printer."

    def __call__(my, cat):
        print "ID: %s Type: %s Code: %s" % cat.Decode()
        return 0

    def error(my, cat, message):
        print message


class command(show):
    "Run a system command on the given code."

    command = filters = None

    def SetCommand(my, command):
        "Set the command string"
        my.command = command

    def SetFilter(my, filters):
        "Set the filters to be applied."
        my.filters = filters

    def __call__(my, cat):
        "Actually run the command."

        type = cat.Type()
        if my.filters and type not in my.filters:
            return 0

        try:
            if my.filters and type[:2] == 'IB': bar = barcode.ISBN(cat.Code())
            elif my.filters and type[:2] == 'UP': bar = barcode.UPC(cat.Code())
            else: bar = barcode.EAN13(cat.Code())
        except ValueError, message:
            my.error(cat, message)
        else:
            if my.command: os.system(my.command % bar.Code)
            else: print "ID: %s Type: %s Code: %s" % \
                  (cat.Id(), type, bar.Code)
        return 0


if __name__ == '__main__':
    if len(sys.argv) == 1:
        handler = show()
    else:
        args = sys.argv[1:]
        types = []
        handler = command()
        while args and args[0][0] == '+':
            types.append(args[0][1:])
            del args[0]
        handler.SetFilter(types)
        handler.SetCommand(string.join(args))

    cuecat().Loop(handler)
    print
